package lime49.lockcrypt.mobile;

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.io.*;
import javax.microedition.io.file.*;
import javax.microedition.rms.*;
import java.io.*;
import java.util.*;

import j2me.util.LinkedHashMap;
import j2me.util.Set;
import j2me.util.Iterator;

import org.bouncycastle.crypto.*;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.modes.*;
import org.bouncycastle.crypto.engines.*;

import org.json.me.*;

// Move options form to separate classes so db location can be changed.

public class LockCrypt extends MIDlet {
    public double LCVER = 1.30;
    private Vector groups;
    private Hashtable accounts;
    private List accountList, log, lstOptions, lstFileLoc;
    private GUI gui;
    private Form frmSynchronise;
    private Display display;
    private boolean shownAlert; // needed to stop execution until alert is dismissed
    private String dbFile = null;
    static final String FIELDSEP = "«";
    public static final String FIELDSPLITTER = "©"; // used to seperate keys and valus for fields
    static final Command cmdExit = new Command("Exit",Command.EXIT,1),
                                 cmdBack = new Command("Back", Command.BACK, 1),
                                 cmdSync = new Command("Sync","Start Sync",Command.SCREEN,2),
                                 cmdAbout = new Command("About","About LockCrypt",Command.SCREEN,2),
                                 cmdOptions = new Command("Options", Command.SCREEN, 2);
    public LockCrypt() {
         display = Display.getDisplay(this);
    }
    
    /**
    * Required method, called when the MIDlet is run
    */
    public void startApp() {
        loadDatabase();
    }

    /**
    * Required method, called when the MIDlet is paused
    */
    public void pauseApp() {
    }
    
    /**
    * Required method, called when the MIDlet is closed
    */
    public void destroyApp(boolean unconditional) {
        display.setCurrent(null);
        notifyDestroyed();
    }
    
    /**
    * Shows the accounts database
    */
    public void displayMain() {
        accountList = new List("Loading...", List.IMPLICIT);
        showItems("LockCrypt", accountList);
        accountList.addCommand(cmdExit);
        //accountList.addCommand(cmdSync);
        accountList.addCommand(cmdAbout);
        accountList.addCommand(cmdOptions);
        accountList.removeCommand(cmdBack);
    }
    
    /**
    * Shows the items from the specified group
    * @param curGroup The name of the group to display
    * @param disp The item to display
    */
    public void showItems(String curGroup, Displayable disp) {
        if(disp instanceof List) {
            //List list = ((List)disp);
            accountList = new List(curGroup, List.IMPLICIT);
            //list.deleteAll();
            Command showCommand = new Command("View", Command.OK, 1);
            accountList.setSelectCommand(showCommand);
            accountList.addCommand(cmdBack);
            accountList.setCommandListener(new ListListener());
            boolean foundAccounts = false;
            for(int i=0;i<groups.size();i++) {
                Group grp = (Group)groups.elementAt(i);
                if(grp.getParentName().equals(curGroup)) {
                    accountList.append(grp.getName(), null);
                    foundAccounts = true;
                }
            }
            //list.append("Acc:"+accounts.size(),null);
            try {
                for(Enumeration keys = accounts.keys(); keys.hasMoreElements();) {
                    Account acc = (Account)accounts.get(keys.nextElement());
                    if(acc.getGroup().equals(curGroup)) {
                        try {
                            Image img = Image.createImage("/images/"+acc.getIcon()+"_16.png");
                            accountList.append(acc.getName(), img);
                        } catch(IOException e) {
                            accountList.append(acc.getName(), null);
                        }
                        foundAccounts = true;
                    }
                }
            } catch(Exception e) {
                accountList.append("E:"+e.getMessage(), null);
                accountList.setTicker(new Ticker(e.toString()));
            }
            if(!foundAccounts) {
                accountList.append("This group is empty", null);
            }
            display.setCurrent(accountList);
        } else {
            disp.setTicker(new Ticker("Not a list"));
        }
    }
    
    /**
    * Synchronises the accounts database over Bluetooth
    */
    public void synchronise() {
        frmSynchronise = new Form("Synchronising");
        display.setCurrent(frmSynchronise);
        try {
            BTClient clientcon = new BTClient(this, frmSynchronise);
        } catch(Exception ignore) {/**/}
    }
    
    /**
    * Shows the specified account
    * @param acc The name of the account to show
    */
    public void showAccount(Account acc) {
        Form accForm = new Form(acc.getName());
        accForm.append(new StringItem("Type: ", acc.getType()));
        
        LinkedHashMap thisInfo = acc.getInfo();
        Set keySet = thisInfo.keySet();
        Iterator iter = keySet.iterator();
        CallCommandListener callListener = new CallCommandListener();
        while(iter.hasNext()) {
        //for(Set keys = thisInfo.keySet(); keys.hasMoreElements();) {
            String thisField = (String)iter.next(),
                   thisFieldPretty = null;
            String thisVal = (String)thisInfo.get(thisField);
            if(thisVal.length() > 0) {
                boolean phoneNumber = false;
                if(thisField.startsWith("~}")) {
                    switch(thisField.charAt(2)) {
                        case '*':
                            thisFieldPretty = thisField.substring(3);
                            break;
                        case '#':
                            thisFieldPretty = thisField.substring(3);
                            phoneNumber = true;
                            break;
                        case '!':
                            continue;
                        default:
                            thisFieldPretty = thisField;
                            break;
                    }
                } else {
                    thisFieldPretty = thisField;
                }
                StringItem currentItem;
                if(phoneNumber) {
                    currentItem = new BetterStringItem(thisFieldPretty+": ",thisVal);
                    currentItem.addCommand(new Command("Call", Command.ITEM, 2));
                    currentItem.setItemCommandListener(callListener);
                } else {
                    currentItem = new StringItem(thisFieldPretty+": ",thisVal);
                }
                accForm.append(currentItem);
            }
        }
        if(acc.getShowNotes()) {
            String notes = acc.getNotes();
            if(notes != null && notes.length() > 0) {
                accForm.append(new StringItem("Notes"+": ",notes));
            }
        }
        
        accForm.addCommand(new Command("Back", Command.BACK, 1));
        accForm.setCommandListener(new ListListener());
        display.setCurrent(accForm);
    }
    
    /**
     * makes a phone call to the specified number
     * @param number The number to call
     */
    private void makeCall(String number) {
        try {
       //     pauseApp();
         //    notifyPaused();
            platformRequest("tel:"+number);
     //       resumeRequest();
        } catch(Exception e) {
            ((Form)display.getCurrent()).append(new String("Not Supported"));
        }
    }
    
    /**
    * Processes the selected menu item and shows either the account if an account was selected or the group
    * @param command The Command to process
    * @param disp The Display context in which to process the command
    */
    private void processSelection(Command command, Displayable disp) {
        if(disp instanceof List) {
            List list = ((List)disp);
            String key = list.getString(list.getSelectedIndex());
            Object obj = accounts.get(key);
            if(obj == null) { // account doesn't exist, it's a group
                showItems(key, disp);
            } else {
                if(obj instanceof Account) {
                    showAccount((Account)obj);
                }
            }
        }
    }
    
    /**
    * Shows the options form, allowing the user to change the database location
    */
    public void showOptionsForm() {
        lstOptions = new List("Options", List.IMPLICIT);
            lstOptions.append("File Location", null);
        Command cmdEdit = new Command("Edit", Command.OK, 1);
            lstOptions.addCommand(cmdEdit);
            lstOptions.addCommand(cmdBack);
            lstOptions.setSelectCommand(cmdEdit);
            lstOptions.setCommandListener(new CommandListener() {
                public void commandAction(Command command, Displayable disp) {
                    int comm = command.getCommandType();
                    switch(comm) {
                        case Command.OK:
                            int selectedOption = lstOptions.getSelectedIndex();
                            switch(selectedOption) {
                                case 0:
                                    showLocationForm(lstOptions);
                                    break;
                            }
                            break;
                        case Command.BACK:
                            displayMain();
                            break;
                    }
                }
            });
        display.setCurrent(lstOptions);
    }
    
    /**
    * Shows the form which allows the user to select a new database location
    * @param disp The Display context in which to process the command
    */
    public void showLocationForm(Displayable disp) {
        final Displayable fromDisp = disp;
        lstFileLoc = new List("File Location", List.IMPLICIT);
        Command cmdSelect = new Command("Select", Command.OK, 1);
        lstFileLoc.addCommand(cmdSelect);
        lstFileLoc.addCommand(new Command("Cancel", Command.CANCEL, 1));
        lstFileLoc.setSelectCommand(cmdSelect);
        lstFileLoc.setCommandListener(new CommandListener() {
            public void commandAction(Command command, Displayable disp2) {
                int comm = command.getCommandType();
                switch(comm) {
                    case Command.OK:
                        try {
                            String fileLoc = lstFileLoc.getString(lstFileLoc.getSelectedIndex());
                            RecordStore dbFileStore = RecordStore.openRecordStore("dbFile", true);
                            dbFileStore.addRecord((fileLoc+"LockCrypt.lce").getBytes("UTF8"),0,(fileLoc+"LockCrypt.lce").length());
                            dbFile = fileLoc+"LockCrypt.lce";
                            showAlert(fromDisp, "Location Set", "The database location has been set to "+dbFile+". If you have not already done so, please copy the database to this location.");
                        } catch(Exception ex) {
                            try {
                                String fileLoc = lstFileLoc.getString(lstFileLoc.getSelectedIndex());
                                RecordStore dbFileStore = RecordStore.openRecordStore("dbFile", true);
                                dbFileStore.addRecord((fileLoc+"LockCrypt.lce").getBytes(),0,(fileLoc+"LockCrypt.lce").length());
                                dbFile = fileLoc+"LockCrypt.lce";
                                showAlert(fromDisp, "Location Set", "The database location has been set to "+dbFile+". If you have not already done so, please copy the database to this location.");
                            } catch(Exception e) {
                                lstFileLoc.append("Ex8a:"+e.getMessage(), null);
                            }
                        }
                        break;
                    case Command.CANCEL:
                        if(dbFile == null) {
                            destroyApp(true);
                        } else {
                            display.setCurrent(fromDisp);
                        }
                        break;
                }
            }
        });
        try {
            for(Enumeration roots = FileSystemRegistry.listRoots(); roots.hasMoreElements();) {
                lstFileLoc.append((String)roots.nextElement(),null);
            }
        } catch(Exception e) {
            lstFileLoc.append("Ex7:Error loading roots", null);
        }
        display.setCurrent(lstFileLoc);
    }
    
    /**
    * Shows the specified message as an alert
    * @param showNext The Displayable to show when the alert is dismissed
    * @param title The alert title
    * @param message The alert message
    */
    public void showAlert(final Displayable showNext, String title, String message) {
        Alert alert = new Alert(title, message, null, AlertType.INFO);
        alert.setTimeout(Alert.FOREVER);
        /*alert.setCommandListener(new CommandListener() {
            public void commandAction(Command command, Displayable disp) {
display.getCurrent().setTicker(new Ticker("R:"+showNext.isShown()+"F:"+display.getCurrent().getClass().toString()));
                if(command.equals(Alert.DISMISS_COMMAND)) {
                    shownAlert = true;
                    if(!showNext.isShown()) {
                       // display.setCurrent(showNext);
                    }
                }
            }
        }); */
        display.setCurrent(alert, showNext);
    }
    
    /**
    * Loads the database from the location specified in the record store
    * Tries to find the settings file. If Found, parses it along with 
    * the initialization vector into a buffer.
    */
    public void loadDatabase() {
        /* Tries to find the settings file
         * If Found, parses it along with the initialization vector into a buffer. */
        accounts = new Hashtable();
        groups = new Vector();
        log = new List("LockCrypt", List.IMPLICIT);
        log.append("Loading...", null);
        log.addCommand(cmdExit);
        log.setCommandListener(new ListListener());
        display.setCurrent(log);
        boolean errorLoading = false;
        FileConnection fconn = null;
//TODO: Check properties->API Permissions for BT send
        try {
            RecordStore dbFileStore = RecordStore.openRecordStore("dbFile", true);
            if(dbFileStore.getNumRecords() == 0) {
                showAlert(log, "LockCrypt Setup", "LockCrypt Mobile requires a database from the desktop version of LockCrypt to function.\nThis file should be saved to one of the root drives of your device.\nOn the next screen, please select a location to use.");
                showLocationForm(log);
            } else {
                int fileSize = dbFileStore.getRecordSize(1);
                if(fileSize>0) {
                    byte[] dbFileBytes = dbFileBytes = new byte[dbFileStore.getRecordSize(1)];
                    int len = dbFileStore.getRecord(1, dbFileBytes, 0);
                    dbFile = new String(dbFileBytes, 0, len);
                } else {
                    showLocationForm(log);
                }
            }
	} catch(Exception e) {
	    log.append("Ex2:"+e.getMessage(), null);
	}
        try {
            //Thread.sleep(5000);
        } catch(Exception e) {}
        while(dbFile == null) {
            try {
                Thread.sleep(1000);
            } catch(Exception e) {}
        }
        if(errorLoading) {
            log.append("File doesn't exist!", null);
            log.append("Please see documentation", null);
        } else {
            try {
                fconn = (FileConnection)Connector.open("file:///"+dbFile, Connector.READ);
                InputStream is = fconn.openInputStream();
		byte[] IV = new byte[16];
                is.read(IV, 0, 16);
                ByteArrayOutputStream bufa = new ByteArrayOutputStream();
                int c = 0;
                while((c = is.read()) != -1) {
                    bufa.write(c);
                }
                is.close();
                fconn.close();
                byte[] cryptText = bufa.toByteArray();
                bufa.close();
                processDatabase(cryptText, IV, true);
            } catch(Exception e) {
                log.append("Tried to load data file:\n"+dbFile+"\n"+e.getMessage(), null);
                log.addCommand(cmdOptions);
                errorLoading = true;
            }
        }
        if(!errorLoading) {
            displayMain();
        }
    }
    
    /**
    * Processes the database and populates the accounts/groups list
    * @param cryptText The encrypted data
    * @param IV The Initialization Vector to use to decrypt the data
    */
    private void processDatabase(byte[] cryptText, byte[] IV, boolean UTF8) {
        Vector acclines = new Vector();
        boolean passwordCorrect = false, errorLoading = false;
        byte[] plainTextBytes = null;
        PWBox pwbox = new PWBox(this);
        boolean firstTry = true;
        while(passwordCorrect == false) {
            String setJCEKey = null;
            if(firstTry) {
                setJCEKey = "";
            } else {
                pwbox.setCurrent();
                while(pwbox.getPassword() == null) {
                    try {
                        this.wait();
                    } catch(Exception ignore) {/**/}
                }
                setJCEKey = pwbox.getPassword();
            }
            display.setCurrent(log);
            try {
                BufferedBlockCipher cipher = new BufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
                cipher.init(false, new ParametersWithIV(new KeyParameter(Utils.getKeySpec(setJCEKey)), IV));
                plainTextBytes = new byte[cipher.getOutputSize(cryptText.length)];
                cipher.processBytes(cryptText, 0, cryptText.length, plainTextBytes, 0);
            } catch(Exception e) {
                if(!firstTry) {
                    log.append("Ex5:"+e.getMessage(), null);
                }
            }
            try {
                    //log.append("Decrypted: "+plainoutputLen+"b", null);
                    //log.append(new String(plainTextBytes), null);
                ByteArrayInputStream plainIn = new ByteArrayInputStream(plainTextBytes);
                    //log.append("Plain: "+plainIn.available(),null);
                StringBuffer buf = new StringBuffer();

                int c = 0;
                for(int i=0;i<=8;i++) {
                    c = plainIn.read();
                    buf.append((char)c);
                }
                if(buf.toString().equals("LockCrypt")) {
                    passwordCorrect = true;
                    firstTry = false;
                } else {
                    //log.append(buf.toString(), null);
                    if(firstTry) {
                        firstTry = false;
                    } else {
                        pwbox.wrongPass();
                    }
                    continue;
                }
                if(UTF8) {
                    while((c = plainIn.read()) != -1) {
                        char ch = (char)c;
                            //log.append(ch+":"+c, null);
                        if(ch == '\n' || ch == '\r' || c < 20) {
                            if(buf.length() > 0) {
                                    //log.append(buf.toString(), null);
                                //acclines.addElement(buf.toString());
                                //acclines.addElement(new String(buf, "UTF8"));
                                byte[] bytes = new byte[buf.length()];
                                for(int i=0;i<buf.length();i++) {
                                    bytes[i] = (byte)buf.charAt(i);
                                }
                                acclines.addElement(new String(bytes, "UTF8"));
                                buf.setLength(0);
                            }
                        } else {
                            buf.append(ch);
                        }
                    }
                } else {
                    while((c = plainIn.read()) != -1) {
                        char ch = (char)c;
                            //log.append(ch+":"+c, null);
                        if(ch == '\n' || ch == '\r' || c < 20) {
                            if(buf.length() > 0) {
                                    //log.append(buf.toString(), null);
                                acclines.addElement(buf.toString());
                                buf.setLength(0);
                            }
                        } else {
                            buf.append(ch);
                        }
                    }
                    plainIn.close();
                }
            } catch(Exception e) {
                log.append("Ex6:"+e.getMessage(), null);
            }
            try {
                if(!acclines.isEmpty()) {
                    JSONObject jso;
                    for(Enumeration e = acclines.elements(); e.hasMoreElements(); ) {
                        String line = (String)e.nextElement();
                        //log.append(line, null);
                        if(line.startsWith("acc")) {
                            try {
                                jso = new JSONObject(line.substring(3));
                                LinkedHashMap fields = new LinkedHashMap();
                                JSONArray jsoFields = jso.getJSONArray("Fields");
                                JSONObject currentField;
                                int numFields = jsoFields.length();
                                for(int i=0;i<numFields;i++) {
                                    currentField = jsoFields.getJSONObject(i);
                                    fields.put(currentField.getString("Name"), currentField.getString("Value"));
                                }
                                try {
                                    String typeName = jso.getString("Type");
                                    String accName = jso.getString("Name");
                                    AccType accType = new AccType(typeName, fields, jso.getBoolean("ShowNotes"), jso.getString("Notes"));
                                    Account acc = new Account(accName, typeName, jso.getString("Icon"), accType, jso.getString("Parent"), jso.getLong("Created"), jso.getLong("Modified"));
                                        //log.append("Created account:"+acc.getName(), null);
                                    accounts.put(accName, acc);
                                        //log.append(accounts.get(name+acc.getName(), null);
                                        //log.append(name+","+icon, null);
                                } catch(Exception er) {
                                    log.append("Ex7:"+er.toString(), null);
                                }
                            } catch(Exception er) {
                                log.append("Ex7f:"+er.toString(), null);
                            }
                        } else if(line.startsWith("grp")) {
                            jso = new JSONObject(line.substring(3));
                            try {
                                groups.addElement(new Group(jso.getString("Name"), jso.getString("Parent")));
                                //log.append("G:"+info[0],null);
                            } catch(Exception er) {
                                log.append("Encoding error, disabling UTF support", null);
                                acclines.setSize(0); // free some memory before trying again
                                processDatabase(cryptText, IV, false);
                            }
                        }
                    }
                    
                    acclines.setSize(0);
                } else {
                    log.append("File empty or corrupt!", null);
                    errorLoading = true;
                }
            } catch (Exception e) {
                log.append("Ex4:"+e.getMessage(), null);
                errorLoading = true;
            }
        }
    }
    
    /**
    * Shows the about dialog
    */
    public void showAbout() {
        Form frmAbout = new Form("About LockCrypt");
        StringItem itmHeading = new StringItem("", "LockCrypt Mobile");
            itmHeading.setFont(Font.getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE));
            itmHeading.setPreferredSize(frmAbout.getWidth(),1);
            itmHeading.setLayout(Item.LAYOUT_CENTER | Item.LAYOUT_NEWLINE_AFTER);
            frmAbout.append(itmHeading);
        
        try {
            Image logosrc = Image.createImage("/images/icon_64.png");
            frmAbout.append(new ImageItem(null, logosrc, Item.LAYOUT_CENTER | Item.LAYOUT_NEWLINE_AFTER, "LockCrypt Logo"));
        } catch(IOException ignore) {/**/}

        StringItem itmVersion = new StringItem("Version "+LCVER, "", Item.PLAIN);
            itmVersion.setFont(Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL));
            itmVersion.setLayout(Item.LAYOUT_CENTER | Item.LAYOUT_TOP | Item.LAYOUT_NEWLINE_AFTER);
            frmAbout.append(itmVersion);
        StringItem itmLink = new StringItem("http://www.lockcrypt.com/", "©2008 Harry Jennerway");
            itmLink.setLayout(Item.LAYOUT_CENTER | Item.LAYOUT_TOP);
            frmAbout.append(itmLink);
        /*StringItem itmCopyright = new StringItem("©2008 Harry Jennerway", "", Item.PLAIN);
            itmCopyright.setFont(Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM));
            itmCopyright.setLayout(Item.LAYOUT_CENTER | Item.LAYOUT_TOP);
            frmAbout.append(itmCopyright); */
            
        frmAbout.addCommand(cmdBack);
        frmAbout.setCommandListener(new CommandListener() {
            public void commandAction(Command command, Displayable disp) {
                displayMain();
            }
        });
        //GUI aboutBox = new GUI(this);
        //display.setCurrent(aboutBox);
        display.setCurrent(frmAbout);
    }
    
    /**
    * Returns the current display
    * @return The current display
    */
    public Display getDisplay() {
        return display;
    }
    
    /**
    * Listens for menu selections and processes commands
    */
    class ListListener implements CommandListener {
        public void commandAction(Command command, Displayable disp) {
                String comm = command.getLabel();
                if(comm.equals("Back")) {
                    displayMain();
                } else if(comm.equals("Exit")) {
                    destroyApp(true);
                } else if(comm.equals("Options")) {
                    showOptionsForm();
                } else if(comm.equals("Sync")) {
                    synchronise();
                } else if(comm.equals("About") || comm.equals("About LockCrypt")) {
                    showAbout();
                } else if(comm.equals("View") || command == List.SELECT_COMMAND) {
                    processSelection(command, disp);
                } else if(command.getCommandType() == Command.ITEM) {
                    //makeCall(command.)
                }
            //processSelection(command, disp);
        }
    }
    
    /**
    * Listens for 'Call' requests
    */
    class CallCommandListener implements ItemCommandListener {
        public void commandAction(Command command, Item item) {
            makeCall(((BetterStringItem)item).getText());
        }
    }
    
    
}
